package kz.pompei.var.atom.concretes;

import static kz.pompei.var.atom.InState.COMMENT;
import static kz.pompei.var.atom.InState.NUMBER;
import static kz.pompei.var.atom.InState.SPACE;
import static kz.pompei.var.atom.InState.STRING;
import static kz.pompei.var.atom.InState.SYMBOL;
import static kz.pompei.var.atom.InState.WORD;
import kz.pompei.var.atom.AtomBracket;
import kz.pompei.var.atom.AtomComment;
import kz.pompei.var.atom.AtomDelimiter;
import kz.pompei.var.atom.AtomEnter;
import kz.pompei.var.atom.AtomNumber;
import kz.pompei.var.atom.AtomReciever;
import kz.pompei.var.atom.AtomSpace;
import kz.pompei.var.atom.AtomStr;
import kz.pompei.var.atom.AtomSymbol;
import kz.pompei.var.atom.AtomWord;
import kz.pompei.var.atom.Atomizer;
import kz.pompei.var.atom.InState;
import kz.pompei.var.error.ErrorReciever;
import kz.pompei.var.error.ErrorUnclosedComment;
import kz.pompei.var.error.ErrorUnclosedString;
import kz.pompei.var.error.ErrorUnknownCharacter;

public class SecondAtomizer implements Atomizer {
  
  private AtomReciever reciever;
  private ErrorReciever errors;
  
  @Override
  public void setReciever(AtomReciever reciever) {
    this.reciever = reciever;
  }
  
  @Override
  public void setErrorReciever(ErrorReciever errors) {
    this.errors = errors;
  }
  
  private InState in;
  private char oc;
  private int commentDeep;
  private boolean multiLineComment;
  
  private int line, col, startLine, startCol;
  
  private final StringBuilder cc = new StringBuilder(1024);
  
  @Override
  public void start() {
    in = SPACE;
    startLine = line = startCol = col = 1;
    cc.setLength(0);
  }
  
  @Override
  public void appendLine(String line) {
    for (int i = 0, C = line.length(); i < C; i++) {
      appendChar(line.charAt(i));
    }
    appendChar('\n');
  }
  
  private void appendChar(char c) {
    innerAppendChar(c);
    if (c == '\n') {
      line++;
      col = 0;
    } else {
      col++;
    }
  }
  
  private void innerAppendChar(char c) {
    if (c == '\r') return;
    if (c == '"' || c == '\'') {
      appendKav(c);
      return;
    }
    if (c == '\n' || c == ';' || c == '|' || c == '(' || c == ')' || c == '[' || c == ']'
        || c == '{' || c == '}') {
      appendDelimiter(c);
      return;
    }
    if (Character.isWhitespace(c)) {
      appendSpace(c);
      return;
    }
    if (c == '+' || c == '-' || c == '=' || c == '/' || c == '\\' || c == '^' || c == '#'
        || c == '%' || c == '@' || c == '!' || c == '<' || c == '>' || c == '.' || c == '*'
        || c == '&' || c == '~' || c == '`' || c == '$' || c == '?') {
      appendSymbol(c);
      return;
    }
    if (Character.isLetter(c) || c == '_') {
      appendLetter(c);
      return;
    }
    if ('0' <= c && c <= '9') {
      appendDigit(c);
      return;
    }
    appendOther(c);
  }
  
  private void appendOther(char c) {
    cc.append(c);
    switch (in) {
    case COMMENT:
    case STRING:
      return;
    default:
      break;
    }
    errors.newError(new ErrorUnknownCharacter(line, col, c));
  }
  
  private void appendDigit(char c) {
    switch (in) {
    case COMMENT:
    case WORD:
    case STRING:
    case NUMBER:
      cc.append(c);
      return;
      
    case SPACE:
      collectSpace();
      cc.append(c);
      in = NUMBER;
      return;
      
    case SYMBOL: {
      if (inEqOneOf(".", "+", "-")) {
        cc.append(c);
        in = NUMBER;
        return;
      }
      collectSymbol();
      cc.append(c);
      in = NUMBER;
      return;
    }
    
    }
  }
  
  private boolean inEqOneOf(String... strings) {
    for (String s : strings) {
      if (s.contentEquals(cc)) return true;
    }
    return false;
  }
  
  private void appendLetter(char c) {
    switch (in) {
    case COMMENT:
    case STRING:
    case NUMBER:
    case WORD:
      cc.append(c);
      return;
      
    case SPACE:
      collectSpace();
      cc.append(c);
      in = WORD;
      return;
      
    case SYMBOL: {
      if ("$".contentEquals(cc)) {
        cc.append(c);
        in = WORD;
        return;
      }
      collectSymbol();
      cc.append(c);
      in = WORD;
      return;
    }
    }
  }
  
  private void appendSymbol(char c) {
    switch (in) {
    case SPACE:
      collectSpace();
      cc.append(c);
      in = SYMBOL;
      return;
      
    case COMMENT: {
      cc.append(c);
      if (multiLineComment) {
        String strCC = cc.toString();
        
        if (strCC.endsWith("*!")) {
          commentDeep--;
          if (commentDeep <= 0) {
            collectComment();
            in = SPACE;
          }
          return;
        }
        
        if (strCC.endsWith("!*")) {
          commentDeep++;
          return;
        }
        
        return;
      }
      return;
    }
    
    case WORD: {
      if (c == '?') {
        cc.append(c);
        collectWord();
        in = SPACE;
        return;
      }
      collectWord();
      cc.append(c);
      in = SYMBOL;
      return;
    }
    
    case STRING:
      cc.append(c);
      return;
      
    case SYMBOL: {
      cc.append(c);
      
      if ("!*".contentEquals(cc)) {
        commentDeep = 1;
        multiLineComment = true;
        in = COMMENT;
        return;
      }
      
      if ("!!".contentEquals(cc)) {
        multiLineComment = false;
        in = COMMENT;
        return;
      }
      
      return;
    }
    
    case NUMBER: {
      if (inEqOneOf(".", "+", "-")) {
        cc.append(c);
        return;
      }
      collectNumber();
      cc.append(c);
      in = SYMBOL;
      return;
    }
    }
  }
  
  private void appendDelimiter(char c) {
    switch (in) {
    case SPACE:
    case WORD:
    case SYMBOL:
    case NUMBER:
      collect(false);
      collectDelimiter(c);
      in = SPACE;
      return;
      
    case COMMENT: {
      if (!multiLineComment && c == '\n') {
        collectComment();
        collectDelimiter(c);
        in = SPACE;
        return;
      }
      
      cc.append(c);
      
      return;
    }
    
    case STRING:
      cc.append(c);
      return;
    }
  }
  
  private void appendKav(char c) {
    switch (in) {
    case COMMENT:
      cc.append(c);
      return;
      
    case STRING: {
      if (last() == '\\') {
        cc.append(c);
        return;
      }
      if (c != oc) {
        cc.append(c);
        return;
      }
      collectStr();
      in = SPACE;
      return;
    }
    
    default:
      collect(false);
      cc.append(c);
      oc = c;
      in = STRING;
      return;
    }
  }
  
  private char last() {
    if (cc.length() == 0) throw new RuntimeException("No last");
    return cc.charAt(cc.length() - 1);
  }
  
  private void appendSpace(char c) {
    switch (in) {
    case SPACE:
    case COMMENT:
    case STRING:
      cc.append(c);
      return;
      
    default:
      collect(false);
      cc.append(c);
      in = SPACE;
      return;
    }
  }
  
  @Override
  public void finish() {
    collect(true);
  }
  
  private void collect(boolean throwError) {
    switch (in) {
    
    case SPACE:
      collectSpace();
      return;
      
    case COMMENT: {
      AtomComment atom = collectComment();
      if (throwError) errors.newError(new ErrorUnclosedComment(atom));
      return;
    }
    
    case WORD:
      collectWord();
      return;
      
    case STRING: {
      AtomStr atom = collectStr();
      if (throwError) errors.newError(new ErrorUnclosedString(atom));
      return;
    }
    
    case SYMBOL:
      collectSymbol();
      return;
      
    case NUMBER:
      collectNumber();
      return;
    }
  }
  
  private AtomComment collectComment() {
    AtomComment ret = new AtomComment(startLine, startCol, cc.toString(), multiLineComment);
    reciever.recieve(ret);
    completeCollect();
    return ret;
  }
  
  private AtomSpace collectSpace() {
    AtomSpace ret = new AtomSpace(startLine, startCol, cc.toString());
    reciever.recieve(ret);
    completeCollect();
    return ret;
  }
  
  private AtomWord collectWord() {
    AtomWord ret = new AtomWord(startLine, startCol, cc.toString());
    reciever.recieve(ret);
    completeCollect();
    return ret;
  }
  
  private AtomStr collectStr() {
    AtomStr ret = new AtomStr(startLine, startCol, cc.toString());
    reciever.recieve(ret);
    completeCollect();
    return ret;
  }
  
  private AtomSymbol collectSymbol() {
    AtomSymbol ret = new AtomSymbol(startLine, startCol, cc.toString());
    reciever.recieve(ret);
    completeCollect();
    return ret;
  }
  
  private AtomNumber collectNumber() {
    AtomNumber ret = new AtomNumber(startLine, startCol, cc.toString());
    reciever.recieve(ret);
    completeCollect();
    return ret;
  }
  
  private void collectDelimiter(char c) {
    if (c == '\n') {
      reciever.recieve(new AtomEnter(line, col));
    } else if (c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c == '}') {
      reciever.recieve(new AtomBracket(line, col, c));
    } else {
      reciever.recieve(new AtomDelimiter(line, col, c));
    }
    completeCollect();
  }
  
  private void completeCollect() {
    cc.setLength(0);
    startLine = line;
    startCol = col;
  }
}
